﻿using System;
using System.Collections.Generic;
using System.Text;
using CompileLib.InteropServices.NVIDIA;
using CompileLib.InteropServices;
using System.Runtime.InteropServices;
using System.IO;
using System.Text.RegularExpressions;

namespace PerformanceTest.GPU
{
    public sealed class DeviceDescriptor
    {
        private int _deviceID;
        private float _h2dPinnedNoWC;
        private float _d2hPinnedNoWC;
        private float _h2dPaged;
        private float _d2hPaged;
        private float _d2dCoalesced;
        private float _d2dUncoalesced;

        public static readonly string h2dPinnedNoWCString = "h2d_pinned";
        public static readonly string d2hPinnedNoWCString = "d2h_pinned";
        public static readonly string h2dPagedString = "h2d_paged";
        public static readonly string d2hPagedString = "d2h_paged";
        public static readonly string d2dCoalescedString = "d2d_coalesced";
        public static readonly string d2dUncoalescedString = "d2d_uncoalesced";


        #region Setters and Getters
        public int deviceID
        {
            get
            {
                return _deviceID;
            }
            set
            {
                _deviceID = value;
            }
        }
        public float h2dPinnedNoWC
        {
            get
            {
                return _h2dPinnedNoWC;
            }
            set
            {
                _h2dPinnedNoWC = value;
            }
        }
        public float d2hPinnedNoWC
        {
            get
            {
                return _d2hPinnedNoWC;
            }
            set
            {
                _d2hPinnedNoWC = value;
            }
        }
        public float h2dPaged
        {
            get
            {
                return _h2dPaged;
            }
            set
            {
                _h2dPaged = value;
            }
        }
        public float d2hPaged
        {
            get
            {
                return _d2hPaged;
            }
            set
            {
                _d2hPaged = value;
            }
        }
        public float d2dCoalesced
        {
            get
            {
                return _d2dCoalesced;
            }
            set
            {
                _d2dCoalesced = value;
            }
        }
        public float d2dUncoalesced
        {
            get
            {
                return _d2dUncoalesced;
            }
            set
            {
                _d2dUncoalesced = value;
            }
        }
        #endregion

        public DeviceDescriptor(int deviceID)
        {
            this.deviceID = deviceID;
            Regex regex;
            Match m;
            StreamReader sr = new StreamReader(String.Format(@".\performance{0}.txt", deviceID));
            string content = sr.ReadToEnd();
            sr.Close();
            regex = new Regex(d2dCoalescedString + @"=(?<value>\d*.\d*)");
            m = regex.Match(content);
            d2dCoalesced = (float)Convert.ToSingle(m.Groups["value"].Value) * (1 << 20) / 1000;
            regex = new Regex(d2dUncoalescedString + @"=(?<value>\d*.\d*)");
            m = regex.Match(content);
            d2dUncoalesced = (float)Convert.ToSingle(m.Groups["value"].Value) * (1 << 20) / 1000;
            regex = new Regex(h2dPagedString + @"=(?<value>\d*.\d*)");
            m = regex.Match(content);
            h2dPaged = (float)Convert.ToSingle(m.Groups["value"].Value) * (1 << 20) / 1000;
            regex = new Regex(d2hPagedString + @"=(?<value>\d*.\d*)");
            m = regex.Match(content);
            d2hPaged = (float)Convert.ToSingle(m.Groups["value"].Value) * (1 << 20) / 1000;
            regex = new Regex(h2dPinnedNoWCString + @"=(?<value>\d*.\d*)");
            m = regex.Match(content);
            h2dPinnedNoWC = (float)Convert.ToSingle(m.Groups["value"].Value) * (1 << 20) / 1000;
            regex = new Regex(d2hPinnedNoWCString + @"=(?<value>\d*.\d*)");
            m = regex.Match(content);
            d2hPinnedNoWC = (float)Convert.ToSingle(m.Groups["value"].Value) * (1 << 20) / 1000;
        }
    }

    public abstract class GPUPerformance
    {
        public abstract float GetH2DTransferTime(long mem_size);
        public abstract float GetD2HTransferTime(long mem_size);
        public abstract float RunMemoryKernel(bool coalesced);
        public abstract void TestPerformance();
    }

    namespace NVIDIA
    {
        public sealed class NVIDIAPerformance : GPUPerformance
        {
            private DeviceDescriptor dd;
            private int deviceID;

            public NVIDIAPerformance(int deviceID)
            {
                this.deviceID = deviceID;
                //dd = new DeviceDescriptor(deviceID);
            }

            /// <summary>
            /// 
            /// </summary>
            /// <param name="b"></param>
            /// <param name="mem_size"></param>
            /// <returns></returns>
            override public float GetH2DTransferTime(long mem_size)
            {
                return mem_size / dd.h2dPaged;
            }

            /// <summary>
            /// 
            /// </summary>
            /// <param name="b"></param>
            /// <param name="mem_size"></param>
            /// <returns></returns>
            override public float GetD2HTransferTime(long mem_size)
            {
                return mem_size / dd.d2hPaged;
            }

            /// <summary>
            /// Tests device memory bandwidth
            /// with coalesced and uncoalesced accesses
            /// </summary>
            /// <param name="coalesced"></param>
            /// <returns>Device memory bandwidth</returns>
            override public float RunMemoryKernel(bool coalesced)
            {
                IntPtr job1 = CUDA.InitEnv(new GPUDevice(deviceID), 3, 1);
                GPUContext ctx1 = CUDA.createContext(job1);
                GPUModule mod1 = CUDA.LoadBin(job1, @".\memoryTest.ptx");
                GPUFunction fun1 = CUDA.LoadFunc(job1, "memoryTest");

                int gridSize = 4096;
                int blockSize = 256;
                int stride = coalesced ? 1 : 32;
                int size = blockSize * gridSize * stride;
                float[] A = new float[size];
                float[] C = new float[size];
                int mem_size = size * sizeof(float);
                Random rand = new Random();
                for (int i = 0; i < size; ++i)
                    A[i] = (float)rand.Next(100);

                //Host allocations
                GCHandle gcHdInput = GCHandle.Alloc(A, GCHandleType.Pinned);
                GCHandle hOutputHandle = GCHandle.Alloc(C, GCHandleType.Pinned);

                float setup = 0.0f;
                CUDA.StartTimer();
                //Device allocations
                int ptrInput = CUDA.MAllocOnGPU(job1, mem_size);
                int ptrOutput = CUDA.MAllocOnGPU(job1, mem_size);
                CUDA.StopTimer();
                setup += CUDA.GetElapsedTime();

                //Data transfer Host to Device
                CUDA.MCpyCPU2GPU(job1, ptrInput, gcHdInput.AddrOfPinnedObject(), mem_size);

                CUDA.StartTimer();
                //Set parameters
                int offset = 0;
                offset = CUDA.SetParamV(job1, fun1, offset, ptrInput);
                offset = CUDA.SetParamV(job1, fun1, offset, ptrOutput);
                offset = CUDA.SetParamI(job1, fun1, offset, stride);
                CUDA.SetParamSize(fun1, offset);

                CUDA.SetBlockShape(fun1, blockSize, 1, 1);

                CUDA.StopTimer();
                setup += CUDA.GetElapsedTime();

                CUDA.StartTimer();
                CUDA.LaunchGrid(fun1, gridSize, 1);
                CUDA.StopTimer();
                float time = CUDA.GetElapsedTime();

                //Data transfer Device to Host
                CUDA.MCpyGPU2CPU(job1, hOutputHandle.AddrOfPinnedObject(), ptrOutput, mem_size);

                //Marshal.Copy(outPtr, C, 0, size);

                gcHdInput.Free();
                hOutputHandle.Free();

                return 2.0f * (mem_size / stride * 1e3f) / (time * (float)(1 << 20));
            }

            /// <summary>
            /// 
            /// </summary>
            override public void TestPerformance()
            {
                string file = String.Format("./performance{0}.txt", deviceID);
                if (!File.Exists(file))
                {
                    StreamWriter sw = new StreamWriter(file);

                    #region PCIe

                    Console.Write("Testing PCIe bandwidth on device {0} ... ", CUDA.GetDeviceName(deviceID));
                    float hostToDeviceBandwidth = CUDA.bandwidthTest(CUDA.memcpyKind.HOST_TO_DEVICE, CUDA.memoryMode.PINNED, false);
                    float deviceToHostBandwidth = CUDA.bandwidthTest(CUDA.memcpyKind.DEVICE_TO_HOST, CUDA.memoryMode.PINNED, false);
                    sw.WriteLine(String.Format(DeviceDescriptor.h2dPinnedNoWCString + "={0}", hostToDeviceBandwidth));
                    sw.WriteLine(String.Format(DeviceDescriptor.d2hPinnedNoWCString + "={0}", deviceToHostBandwidth));
                    hostToDeviceBandwidth = CUDA.bandwidthTest(CUDA.memcpyKind.HOST_TO_DEVICE, CUDA.memoryMode.PAGEABLE, false);
                    deviceToHostBandwidth = CUDA.bandwidthTest(CUDA.memcpyKind.DEVICE_TO_HOST, CUDA.memoryMode.PAGEABLE, false);
                    sw.WriteLine(String.Format(DeviceDescriptor.h2dPagedString + "={0}", hostToDeviceBandwidth));
                    sw.WriteLine(String.Format(DeviceDescriptor.d2hPagedString + "={0}", deviceToHostBandwidth));
                    Console.WriteLine("done\n");

                    #endregion

                    #region Device Memory

                    Console.Write("Testing Device memory bandwidth on device {0} ... ", CUDA.GetDeviceName(deviceID));

                    float coalescedBandwidth = RunMemoryKernel(true);

                    float uncoalescedBandwidth = RunMemoryKernel(false);

                    sw.WriteLine(String.Format(DeviceDescriptor.d2dCoalescedString + "={0}", coalescedBandwidth));
                    sw.WriteLine(String.Format(DeviceDescriptor.d2dUncoalescedString + "={0}", uncoalescedBandwidth));
                    sw.Close();
                    Console.WriteLine("done\n");

                    #endregion
                }
                else
                {
                    Console.WriteLine("Tests not needed for device {0}.", CUDA.GetDeviceName(deviceID));
                }

                dd = new DeviceDescriptor(deviceID);
            }
        }
    }
}
